本篇介紹 ES2018 (ES9) 提供的 Promise.prototype.finally()
。
下面是幾個非同步處理很常見的情境:
以上情境都有一個共通點:不管做什麼事,最後都要做某件事。
也許你會想到 try-finally
,會希望非同步處理的 Promise
上也有 finally 的功能 (我自己是沒想過啦 XD),這就是今天要介紹的 Promise.prototype.finally()
!
在過去原生的 Promise
沒有提供 finally 功能時,很多 library 都在非同步處理的 API 上實作了 finally()
方法,此方法是用來註冊一個在 promise settled 時 (即 fulfilled 或 rejected) invoke 用的 callback。
更多 library 的實作可參閱:
在 ES2018 (ES9) 提供了 Promise.prototype.finally()
新的 Promise
method。當 promise settled 時 (即 fulfilled 或 rejected),會執行指定的 callback。
先說明什麼是 promise chain,因為之後會常常看到這個專有名詞。
將多個 Promise
串在一起,以表達一個序列的非同步執行步驟,而這個序列就是 promise chain。
那為何是 chain?因為每次在 Promise
上呼叫 .then()
、.catch()
、.finally()
等 Promise
method 時,都會建立並回傳新的 Promise
。例如:
Promise.resolve('OK')
.then(result => {
console.log(result);
return Promise.resolve('Hi')
})
.then(result => {
console.log(result);
return Promise.reject('Oops');
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log('finally');
});
// OK
// Hi
// Oops
// finally
Promise.prototype.finally()
的回傳值永遠是 Promise
Promise.prototype.finally()
的回傳值永遠是 Promise
物件,該 promise 可能會 fulfilled 或 rejected,那何時會 fulfilled?還是會 rejected?
先講結論:要看你的 promise chain 是怎麼寫的
.finally()
的前一個 Promise 是 fulfilled,那 .finally()
回傳的 Promise
就會是 fulfilled.finally()
的前一個 Promise 是 rejected,那 .finally()
回傳的 Promise
就會是 rejected先看幾個範例:
假設我先執行 Promise.resolve('OK')
,該 promise 會立即 fulfilled,將 OK
傳給 .then()
的 callback,所以第一個輸出訊息會是 OK
,接著執行 .finally()
,並將 .finally()
的回傳值存在一個名為 promiseA
的變數:
let promiseA = Promise.resolve('OK')
.then(result => {
console.log(result);
})
.finally(() => {
console.log('finally');
});
// OK
// finally
接著印出 promiseA
,它是一個 Promise
物件,該 promise 已經 fulfilled 了,且 fulfilled 的值為 undefined
:
console.log(promiseA);
// Promise {<fulfilled>: undefined}
那為何 fulfilled 的值會是 undefined
,因為在 promise chain 中,.finally()
的前一個 Promise 是 .then()
回傳的,而 .then()
的 callback 沒有回傳值,所以才會是 undefined
。
所以不要搞錯了,
promiseA
存的不是Promise.resolve('OK')
回傳的Promise
物件,而是最後一個 promise chain 的。
那再看下一個範例,這次拿到 .then()
這個步驟,一樣將的回傳值存起來,存在一個名為 promiseB
的變數:
let promiseB = Promise.resolve('OK')
.finally(() => {
console.log('finally');
});
// finally
接著印出 promiseB
,該 promise 一樣已經 fulfilled 了,但這次 fulfilled 的值是 OK
:
console.log(promiseB);
// Promise {<fulfilled>: "OK"}
為什麼會是 OK
?因為在 promise chain 中,.finally()
的前一個 Promise 是 Promise.resolve('OK')
回傳的,該 Promise
fulfilled 的值就是 OK
,所以才會是 OK
。
所以就如同前面結論說的,.finally()
回傳的 Promise
是 fulfilled 還是 rejected,是依據 promise chain 中前一個 Promise 來決定的。
Promise.prototype.finally()
的 callbackPromise.prototype.finally()
的 callback 沒有 argument.then()
和 .catch()
的 callback 會有 argument,而該 argumemt 是在 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。
但 Promise.prototype.finally()
的 callback 是沒有 argument 的,若你還是寫了 argument,其值也會是 undefined
,不管 promise chain 中的前一個 Promise 的 fulfilled 或 rejected:
Promise.resolve('OK')
.finally(value => {
console.log(value);
});
// undefined
Promise.reject('Oops')
.finally(value => {
console.log(value);
});
// undefined
Promise.prototype.finally()
的 callback 會被忽略 return
Promise.prototype.finally()
的 callback 中的 return
會被忽略,但回傳的 Promise
的 fulfilled 值或 rejected 值會是 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。:
例如:Promise.resolve('OK')
會立即 fulfilled,接著在 .finally()
內 return
會被忽略,但 .finally()
回傳的 Promise
的 fulfilled 值會跟 Promise.resolve('OK')
回傳的 fulfilled 值相同。
Promise.resolve('OK')
.finally(() => {
console.log('finally...');
return 'finally';
})
.then(value => {
console.log(value);
});
若拆開 promise chain 就會更容易看出來:
let promiseA = Promise.resolve('OK');
console.log(promiseA);
// Promise {<fulfilled>: "OK"}
let promiseB = promiseA.finally(() => {
console.log('finally...');
return 'finally';
});
// finally...
console.log(promiseB);
// Promise {<fulfilled>: "OK"}
let promiseC = promiseB.then(value => {
console.log(value);
});
// OK
console.log(promiseC);
// Promise {<fulfilled>: "undefined"}
promise rejected 的情況也一樣,你可以試著將上面的 Promise.resolve('OK')
改成 Promise.reject('Oops')
觀察看看。
Promise.prototype.finally()
vs. finally
clause先來看兩者的寫法。
下面是 Promise.prototype.finally()
的用法:
Promise.resolve('OK')
.then(result => {
console.log(result);
})
.catch(error => {
console.log('error');
})
.finally(() => {
console.log('finally');
});
// OK
// finally
而下面是 try
陳述句中 finally
clause 的用法:
try {
console.log('OK');
} catch (error) {
console.log('error');
} finally {
console.log('finally');
}
// OK
// finally
兩者有些地方很相識,但用法和行為都不同,下面會提出它們的不同之處。
return
值Promise.prototype.finally()
會回傳 Promise
,該 Promise
可能會 fulfilled 或 rejected (前面有說明)。
而 finally
只是 try
陳述句中的 clause,若在 finally
clause 內 return
某個值會成為 function 的回傳值。
例如:在 func()
函數中,finally
clause 內 return
的 func
就成為此函數的回傳值:
function func() {
try {
console.log('try');
} catch (error) {
console.log('catch');
} finally {
console.log('finally');
return 'func';
}
}
let result = func();
// try
// finally
console.log(result);
// "func"
throw
值若在 finally
clause 內使用 throw
,需要讓另一個 try-catch
來捕捉錯誤:
function func() {
try {
console.log('try');
} finally {
console.log('finally');
throw new Error('Oops');
}
}
try {
func();
} catch(error) {
console.log(error);
}
// try
// finally
// Error: Oops
// at func (<anonymous>:6:11)
// at <anonymous>:11:3
而在 Promise.prototype.finally()
的 callback 中使用 throw
,會讓回傳的 Promise
rejected:
let promiseA = Promise.resolve('OK')
.then(result => {
console.log(result);
});
// OK
console.log(promiseA);
// Promise {<fulfilled>: undefined}
let promiseB = promiseA.finally(() => {
console.log('finally');
throw new Error('Oops');
});
// finally
// Uncaught (in promise) Error: Oops
// at <anonymous>:3:11
// at <anonymous>
console.log(promiseB);
// Promise {<rejected>: Error: Oops
// at <anonymous>:3:11
// at <anonymous>}
Promise.prototype.finally()
和 finally
clause 的其中一個共通點就是一定會執行。
finally
clause 一定會在最後執行先來說明 finally
clause。
在函數內的 try
clause 或 catch
clause 裡面 return
某個值,函數會在回傳該值之前,先執行 finally
clause 內的程式碼 (所以 finally
就如其名,真的是「最後」)。
例如:在 try
clause 內 return
值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally
clause 內的程式碼:
function func() {
try {
console.log('try');
return 'func';
} catch (error) {
console.log('catch');
} finally {
console.log('finally');
}
}
console.log(func());
// try
// finally
// "func"
另一個範例:在 catch
clause 內 return
值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally
clause 內的程式碼:
function func() {
try {
console.log(data);
} catch (error) {
console.log('catch');
return 'func';
} finally {
console.log('finally');
}
}
console.log(func());
// catch
// finally
// "func"
Promise.prototype.finally()
的 callback 都會執行不管 Promise
是 fulfilled 或 rejected 都會執行 Promise.prototype.finally()
內的 callback。
例如:Promise.resolve('OK')
會回傳的 promise 立即 fulfilled 後,會執行 .finally()
的 callback:
let promiseA = Promise.resolve('OK')
.finally(() => {
console.log('finally');
});
// finally
console.log(promiseA);
// Promise {<fulfilled>: "OK"}
另一個例子:Promise.reject('Oops')
會回傳的 promise 立即 rejected 後,會執行 .finally()
的 callback:
let promiseB = Promise.reject('Oops')
.finally(() => {
console.log('finally');
});
console.log(promiseB);
// finally
前面提到一些情境,就拿其中一個作為範例。
假設進入某頁面時,會立即發 AJAX request,在拿到 response 之前都會顯示「正在載入...」的訊息,不管是拿到 response,還是發生錯誤,都會隱藏「正在載入...」。
範例程式碼如下:
let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';
function fetchData(url) {
return fetch(url)
.then(response => {
console.log('isLoading:', isLoading);
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return response.json();
} else {
throw new TypeError(`Oops, we haven't got JSON!`);
}
})
.then(json => {
console.log('Success');
return json;
})
.catch(error => {
console.log(error);
})
.finally(() => {
isLoading = false;
console.log('isLoading:', isLoading);
});
}
// 試著換成 fetchData(HTML_API)
fetchData(JSON_API).then(data => {
console.log(data);
});
之後會提到
?.
Optional Chaining 運算子。
若 API 的 Content-Type
是 application/json
(即 fetch(JSON_API)
這個 AJAX response),promise 就會 fulfilled 列印出 API 資料,並且在 finally 時將 isLoading
設為 false
,所以輸出如下:
fetchData(JSON_API).then(data => {
console.log(data);
});
// isLoading: true
// Success
// isLoading: false
// {userId: 1, id: 1, title: "..."}
若 API 的 Content-Type
不是 application/json
(即 fetch(HTML_API)
這個 AJAX response),promise 就會 rejected 列印出錯誤訊息,並且在 finally 時將 isLoading
設為 false
,所以輸出如下:
fetchData(HTML_API).then(data => {
console.log(data);
});
// isLoading: true
// TypeError: Oops, we haven't got JSON!
// isLoading: false
// undefined
因為不管是 .then()
或 .catch()
都要執行 isLoading = false
,那更好的作法就是統一在 .finally()
執行 isLoading = false
,這樣就不用寫重複的邏輯了。
若上面的範例改用 async
/ await
的寫法也許會像這樣:
let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';
async function fetchData(url) {
try {
const response = await fetch(url);
console.log('isLoading:', isLoading);
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
console.log('Success');
return response.json();
} else {
throw new TypeError(`Oops, we haven't got JSON!`);
}
} catch(error) {
console.log(error);
} finally {
isLoading = false;
console.log('isLoading:', isLoading);
}
}